/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/gift_auctions.h"

#include "api/api_hash.h"
#include "api/api_premium.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "main/main_session.h"

namespace Data {

GiftAuctions::GiftAuctions(not_null<Main::Session*> session)
: _session(session)
, _timer([=] { checkSubscriptions(); }) {
	crl::on_main(_session, [=] {
		rpl::merge(
			_session->data().chatsListChanges(),
			_session->data().chatsListLoadedEvents()
		) | rpl::filter(
			!rpl::mappers::_1
		) | rpl::take(1) | rpl::start_with_next([=] {
			requestActive();
		}, _lifetime);
	});
}

GiftAuctions::~GiftAuctions() = default;

rpl::producer<GiftAuctionState> GiftAuctions::state(const QString &slug) {
	return [=](auto consumer) {
		auto lifetime = rpl::lifetime();

		auto &entry = _map[slug];
		if (!entry) {
			entry = std::make_unique<Entry>();
		}
		const auto raw = entry.get();

		raw->changes.events() | rpl::start_with_next([=] {
			consumer.put_next_copy(raw->state);
		}, lifetime);

		const auto now = crl::now();
		if (raw->state.subscribedTill < 0
			|| raw->state.subscribedTill >= now) {
			consumer.put_next_copy(raw->state);
		} else if (raw->state.subscribedTill >= 0) {
			request(slug);
		}

		return lifetime;
	};
}

void GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) {
	if (const auto entry = find(data.vgift_id().v)) {
		const auto was = myStateKey(entry->state);
		apply(entry, data.vstate());
		entry->changes.fire({});
		if (was != myStateKey(entry->state)) {
			_activeChanged.fire({});
		}
	} else {
		requestActive();
	}
}

void GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) {
	if (const auto entry = find(data.vgift_id().v)) {
		const auto was = myStateKey(entry->state);
		apply(entry, data.vuser_state());
		entry->changes.fire({});
		if (was != myStateKey(entry->state)) {
			_activeChanged.fire({});
		}
	} else {
		requestActive();
	}
}

void GiftAuctions::requestAcquired(
		uint64 giftId,
		Fn<void(std::vector<Data::GiftAcquired>)> done) {
	Expects(done != nullptr);

	_session->api().request(MTPpayments_GetStarGiftAuctionAcquiredGifts(
		MTP_long(giftId)
	)).done([=](const MTPpayments_StarGiftAuctionAcquiredGifts &result) {
		const auto &data = result.data();

		const auto owner = &_session->data();
		owner->processUsers(data.vusers());
		owner->processChats(data.vchats());

		const auto &list = data.vgifts().v;
		auto gifts = std::vector<Data::GiftAcquired>();
		gifts.reserve(list.size());
		for (const auto &gift : list) {
			const auto &data = gift.data();
			gifts.push_back({
				.to = owner->peer(peerFromMTP(data.vpeer())),
				.message = (data.vmessage()
					? Api::ParseTextWithEntities(_session, *data.vmessage())
					: TextWithEntities()),
				.date = data.vdate().v,
				.bidAmount = int64(data.vbid_amount().v),
				.round = data.vround().v,
				.number = data.vgift_num().value_or_empty(),
				.position = data.vpos().v,
				.nameHidden = data.is_name_hidden(),
			});
		}
		if (const auto entry = find(giftId)) {
			const auto count = int(gifts.size());
			if (entry->state.my.gotCount != count) {
				entry->state.my.gotCount = count;
				entry->changes.fire({});
			}
		}
		done(std::move(gifts));
	}).fail([=] {
		done({});
	}).send();
}

std::optional<Data::UniqueGiftAttributes> GiftAuctions::attributes(
		uint64 giftId) const {
	const auto i = _attributes.find(giftId);
	return (i != end(_attributes) && i->second.waiters.empty())
		? i->second.lists
		: std::optional<Data::UniqueGiftAttributes>();
}

void GiftAuctions::requestAttributes(uint64 giftId, Fn<void()> ready) {
	auto &entry = _attributes[giftId];
	entry.waiters.push_back(std::move(ready));
	if (entry.waiters.size() > 1) {
		return;
	}
	_session->api().request(MTPpayments_GetStarGiftUpgradeAttributes(
		MTP_long(giftId)
	)).done([=](const MTPpayments_StarGiftUpgradeAttributes &result) {
		const auto &attributes = result.data().vattributes().v;
		auto &entry = _attributes[giftId];
		auto &info = entry.lists;
		info.models.reserve(attributes.size());
		info.patterns.reserve(attributes.size());
		info.backdrops.reserve(attributes.size());
		for (const auto &attribute : attributes) {
			attribute.match([&](const MTPDstarGiftAttributeModel &data) {
				info.models.push_back(Api::FromTL(_session, data));
			}, [&](const MTPDstarGiftAttributePattern &data) {
				info.patterns.push_back(Api::FromTL(_session, data));
			}, [&](const MTPDstarGiftAttributeBackdrop &data) {
				info.backdrops.push_back(Api::FromTL(data));
			}, [](const MTPDstarGiftAttributeOriginalDetails &data) {
			});
		}
		for (const auto &ready : base::take(entry.waiters)) {
			ready();
		}
	}).fail([=] {
		for (const auto &ready : base::take(_attributes[giftId].waiters)) {
			ready();
		}
	}).send();
}

rpl::producer<ActiveAuctions> GiftAuctions::active() const {
	return _activeChanged.events_starting_with_copy(
		rpl::empty
	) | rpl::map([=] {
		return collectActive();
	});
}

rpl::producer<bool> GiftAuctions::hasActiveChanges() const {
	const auto has = hasActive();
	return _activeChanged.events(
	) | rpl::map([=] {
		return hasActive();
	}) | rpl::combine_previous(
		has
	) | rpl::filter([=](bool previous, bool current) {
		return previous != current;
	}) | rpl::map([=](bool previous, bool current) {
		return current;
	});
}

bool GiftAuctions::hasActive() const {
	for (const auto &[slug, entry] : _map) {
		if (myStateKey(entry->state)) {
			return true;
		}
	}
	return false;
}

void GiftAuctions::checkSubscriptions() {
	const auto now = crl::now();
	auto next = crl::time();
	for (const auto &[slug, entry] : _map) {
		const auto raw = entry.get();
		const auto till = raw->state.subscribedTill;
		if (till <= 0 || !raw->changes.has_consumers()) {
			continue;
		} else if (till <= now) {
			request(slug);
		} else {
			const auto timeout = till - now;
			if (!next || timeout < next) {
				next = timeout;
			}
		}
	}
	if (next) {
		_timer.callOnce(next);
	}
}

auto GiftAuctions::myStateKey(const GiftAuctionState &state) const
-> MyStateKey {
	if (!state.my.bid) {
		return {};
	}
	auto min = 0;
	for (const auto &level : state.bidLevels) {
		if (level.position > state.gift->auctionGiftsPerRound) {
			break;
		} else if (!min || min > level.amount) {
			min = level.amount;
		}
	}
	return {
		.bid = int(state.my.bid),
		.position = MyAuctionPosition(state),
		.version = state.version,
	};
}

ActiveAuctions GiftAuctions::collectActive() const {
	auto result = ActiveAuctions();
	result.list.reserve(_map.size());
	for (const auto &[slug, entry] : _map) {
		const auto raw = &entry->state;
		if (raw->gift && raw->my.date) {
			result.list.push_back(raw);
		}
	}
	return result;
}

uint64 GiftAuctions::countActiveHash() const {
	auto result = Api::HashInit();
	for (const auto &active : collectActive().list) {
		Api::HashUpdate(result, active->version);
		Api::HashUpdate(result, active->my.date);
	}
	return Api::HashFinalize(result);
}

void GiftAuctions::requestActive() {
	if (_activeRequestId) {
		return;
	}
	_activeRequestId = _session->api().request(
		MTPpayments_GetStarGiftActiveAuctions(MTP_long(countActiveHash()))
	).done([=](const MTPpayments_StarGiftActiveAuctions &result) {
		result.match([=](const MTPDpayments_starGiftActiveAuctions &data) {
			const auto owner = &_session->data();
			owner->processUsers(data.vusers());
			owner->processChats(data.vchats());

			auto giftsFound = base::flat_set<QString>();
			const auto &list = data.vauctions().v;
			giftsFound.reserve(list.size());
			for (const auto &auction : list) {
				const auto &data = auction.data();
				auto gift = Api::FromTL(_session, data.vgift());
				const auto slug = gift ? gift->auctionSlug : QString();
				if (slug.isEmpty()) {
					LOG(("Api Error: Bad auction gift."));
					continue;
				}
				auto &entry = _map[slug];
				if (!entry) {
					entry = std::make_unique<Entry>();
				}
				const auto raw = entry.get();
				if (!raw->state.gift) {
					raw->state.gift = std::move(gift);
				}
				apply(raw, data.vstate());
				apply(raw, data.vuser_state());
				giftsFound.emplace(slug);
			}
			for (const auto &[slug, entry] : _map) {
				const auto my = &entry->state.my;
				if (my->date && !giftsFound.contains(slug)) {
					my->to = nullptr;
					my->minBidAmount = 0;
					my->bid = 0;
					my->date = 0;
					my->returned = false;
					giftsFound.emplace(slug);
				}
			}
			for (const auto &slug : giftsFound) {
				_map[slug]->changes.fire({});
			}
			_activeChanged.fire({});
		}, [](const MTPDpayments_starGiftActiveAuctionsNotModified &) {
		});
	}).send();
}

void GiftAuctions::request(const QString &slug) {
	auto &entry = _map[slug];
	Assert(entry != nullptr);

	const auto raw = entry.get();
	if (raw->requested) {
		return;
	}
	raw->requested = true;
	_session->api().request(MTPpayments_GetStarGiftAuctionState(
		MTP_inputStarGiftAuctionSlug(MTP_string(slug)),
		MTP_int(raw->state.version)
	)).done([=](const MTPpayments_StarGiftAuctionState &result) {
		raw->requested = false;
		const auto &data = result.data();

		_session->data().processUsers(data.vusers());
		_session->data().processChats(data.vchats());

		raw->state.gift = Api::FromTL(_session, data.vgift());
		if (!raw->state.gift) {
			return;
		}
		const auto timeout = data.vtimeout().v;
		const auto ms = timeout * crl::time(1000);
		raw->state.subscribedTill = ms ? (crl::now() + ms) : -1;

		const auto was = myStateKey(raw->state);
		apply(raw, data.vstate());
		apply(raw, data.vuser_state());
		if (raw->changes.has_consumers()) {
			raw->changes.fire({});
			if (ms && (!_timer.isActive() || _timer.remainingTime() > ms)) {
				_timer.callOnce(ms);
			}
		}
		if (was != myStateKey(raw->state)) {
			_activeChanged.fire({});
		}
	}).send();
}

GiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const {
	for (const auto &[slug, entry] : _map) {
		if (entry->state.gift && entry->state.gift->id == giftId) {
			return entry.get();
		}
	}
	return nullptr;
}

void GiftAuctions::apply(
		not_null<Entry*> entry,
		const MTPStarGiftAuctionState &state) {
	apply(&entry->state, state);
}

void GiftAuctions::apply(
		not_null<GiftAuctionState*> entry,
		const MTPStarGiftAuctionState &state) {
	Expects(entry->gift.has_value());

	state.match([&](const MTPDstarGiftAuctionState &data) {
		const auto version = data.vversion().v;
		if (entry->version >= version) {
			return;
		}
		const auto owner = &_session->data();
		entry->startDate = data.vstart_date().v;
		entry->endDate = data.vend_date().v;
		entry->minBidAmount = data.vmin_bid_amount().v;
		const auto &levels = data.vbid_levels().v;
		entry->bidLevels.clear();
		entry->bidLevels.reserve(levels.size());
		for (const auto &level : levels) {
			auto &bid = entry->bidLevels.emplace_back();
			const auto &data = level.data();
			bid.amount = data.vamount().v;
			bid.position = data.vpos().v;
			bid.date = data.vdate().v;
		}
		const auto &top = data.vtop_bidders().v;
		entry->topBidders.clear();
		entry->topBidders.reserve(top.size());
		for (const auto &user : top) {
			entry->topBidders.push_back(owner->user(UserId(user.v)));
		}
		entry->nextRoundAt = data.vnext_round_at().v;
		entry->giftsLeft = data.vgifts_left().v;
		entry->currentRound = data.vcurrent_round().v;
		entry->totalRounds = data.vtotal_rounds().v;
		const auto &rounds = data.vrounds().v;
		entry->roundParameters.clear();
		entry->roundParameters.reserve(rounds.size());
		for (const auto &round : rounds) {
			round.match([&](const MTPDstarGiftAuctionRound &data) {
				entry->roundParameters.push_back({
					.number = data.vnum().v,
					.duration = data.vduration().v,
				});
			}, [&](const MTPDstarGiftAuctionRoundExtendable &data) {
				entry->roundParameters.push_back({
					.number = data.vnum().v,
					.duration = data.vduration().v,
					.extendTop = data.vextend_top().v,
					.extendDuration = data.vextend_window().v,
				});
			});
		}
		entry->averagePrice = 0;
	}, [&](const MTPDstarGiftAuctionStateFinished &data) {
		entry->averagePrice = data.vaverage_price().v;
		entry->startDate = data.vstart_date().v;
		entry->endDate = data.vend_date().v;
		entry->minBidAmount = 0;
		entry->nextRoundAt
			= entry->currentRound
			= entry->totalRounds
			= entry->giftsLeft
			= entry->version
			= 0;
	}, [&](const MTPDstarGiftAuctionStateNotModified &data) {
	});
}

void GiftAuctions::apply(
		not_null<Entry*> entry,
		const MTPStarGiftAuctionUserState &state) {
	apply(&entry->state.my, state);
}

void GiftAuctions::apply(
		not_null<StarGiftAuctionMyState*> entry,
		const MTPStarGiftAuctionUserState &state) {
	const auto &data = state.data();
	entry->to = data.vbid_peer()
		? _session->data().peer(peerFromMTP(*data.vbid_peer())).get()
		: nullptr;
	entry->minBidAmount = data.vmin_bid_amount().value_or(0);
	entry->bid = data.vbid_amount().value_or(0);
	entry->date = data.vbid_date().value_or(0);
	entry->gotCount = data.vacquired_count().v;
	entry->returned = data.is_returned();
}

int MyAuctionPosition(const GiftAuctionState &state) {
	const auto &levels = state.bidLevels;
	for (auto i = begin(levels), e = end(levels); i != e; ++i) {
		if (i->amount < state.my.bid
			|| (i->amount == state.my.bid && i->date >= state.my.date)) {
			return i->position;
		}
	}
	return (levels.empty() ? 0 : levels.back().position) + 1;
}

} // namespace Data
